xtask\tasks\fmt/
workspace.rs1use crate::Xtask;
5use anyhow::Context;
6use clap::Parser;
7use rayon::prelude::*;
8use serde::Deserialize;
9use std::cell::Cell;
10use std::collections::HashSet;
11use std::path::PathBuf;
12use toml_edit::Item;
13use toml_edit::TableLike;
14use toml_edit::Value;
15
16#[derive(Parser)]
17#[clap(about = "Verify that all Cargo.toml files are valid and in the workspace")]
18pub struct VerifyWorkspace;
19
20static WORKSPACE_EXCEPTIONS: &[(&str, &[&str])] = &[
22 ("disk_blob", &["tokio"]),
26 ("mesh_rpc", &["tokio"]),
30];
31
32impl Xtask for VerifyWorkspace {
33 fn run(self, ctx: crate::XtaskCtx) -> anyhow::Result<()> {
34 let excluded = {
35 let contents = fs_err::read_to_string("Cargo.toml")?;
37 let parsed = contents.parse::<toml_edit::DocumentMut>()?;
38
39 if let Some(excluded) = parsed
40 .as_table()
41 .get("workspace")
42 .and_then(|w| w.get("exclude"))
43 .and_then(|e| e.as_array())
44 {
45 let mut exclude = Vec::new();
46 for entry in excluded {
47 let entry = entry.as_str().unwrap();
48 exclude.push(
49 std::path::absolute(entry)
50 .with_context(|| format!("cannot exclude {}", entry))?,
51 );
52 }
53 exclude
54 } else {
55 Vec::new()
56 }
57 };
58
59 let entries = ignore::WalkBuilder::new(ctx.root)
61 .filter_entry(move |e| {
62 for path in excluded.iter() {
63 if e.path().starts_with(path) {
64 return false;
65 }
66 }
67
68 true
69 })
70 .build()
71 .filter_map(|entry| match entry {
72 Ok(entry) if entry.file_name() == "Cargo.toml" => Some(entry.into_path()),
73 Err(err) => {
74 log::error!("error when walking over subdirectories: {}", err);
75 None
76 }
77 _ => None,
78 })
79 .collect::<Vec<_>>();
80
81 let manifests = workspace_manifests()?;
82
83 let all_present = entries.iter().all(|entry| {
84 if !manifests.contains(entry) {
85 log::error!("Error: {} is not present in the workspace", entry.display());
86 false
87 } else {
88 true
89 }
90 });
91
92 let dependencies_valid = manifests.par_iter().all(|entry| {
93 if let Err(err) = verify_dependencies(entry) {
94 log::error!("Error: failed to verify {}: {:#}", entry.display(), err);
95 false
96 } else {
97 true
98 }
99 });
100
101 if !all_present || !dependencies_valid {
102 anyhow::bail!("found invalid Cargo.toml");
103 }
104
105 Ok(())
106 }
107}
108
109#[derive(Deserialize)]
110struct CargoMetadata {
111 packages: Vec<Package>,
112 workspace_root: PathBuf,
113}
114
115#[derive(Deserialize)]
116struct Package {
117 manifest_path: PathBuf,
118}
119
120fn workspace_manifests() -> anyhow::Result<HashSet<PathBuf>> {
121 let json = xshell::Shell::new()?
122 .cmd("cargo")
123 .arg("metadata")
124 .arg("--no-deps")
125 .arg("--format-version=1")
126 .read()?;
127 let metadata: CargoMetadata =
128 serde_json::from_str(&json).context("failed to parse JSON result")?;
129
130 Ok(metadata
131 .packages
132 .into_iter()
133 .map(|p| p.manifest_path)
134 .chain([metadata.workspace_root.join("Cargo.toml")])
135 .collect())
136}
137
138fn verify_dependencies(path: &PathBuf) -> Result<(), anyhow::Error> {
139 let contents = fs_err::read_to_string(path)?;
141 let parsed = contents.parse::<toml_edit::DocumentMut>()?;
142
143 let package_name = match parsed
144 .as_table()
145 .get("package")
146 .and_then(|p| p.get("name"))
147 .and_then(|n| n.as_str())
148 {
149 Some(name) => name,
150 None => return Ok(()), };
152
153 let mut dep_tables = Vec::new();
154 for (name, v) in parsed.iter() {
155 match name {
156 "dependencies" | "build-dependencies" | "dev-dependencies" => {
157 dep_tables.push(v.as_table_like().unwrap())
158 }
159 "target" => {
160 let flattened = v
161 .as_table_like()
162 .unwrap()
163 .iter()
164 .flat_map(|(_, v)| v.as_table_like().unwrap().iter());
165
166 for (k, v) in flattened {
167 match k {
168 "dependencies" | "build-dependencies" | "dev-dependencies" => {
169 dep_tables.push(v.as_table_like().unwrap())
170 }
171 _ => {}
172 }
173 }
174 }
175 _ => {}
176 }
177 }
178
179 let found_bad_deps = Cell::new(false);
180
181 let handle_non_workspaced_dep = |dep_name| {
182 let allowed = WORKSPACE_EXCEPTIONS
183 .iter()
184 .find_map(|&(p, crates)| (p == package_name).then_some(crates))
185 .unwrap_or(&[]);
186
187 if allowed.contains(&dep_name) {
188 log::debug!(
189 "{} contains non-workspaced dependency {}. Allowed by exception.",
190 package_name,
191 dep_name
192 );
193 } else {
194 found_bad_deps.set(true);
195 log::error!(
196 "{} contains non-workspaced dependency {}. Please move this dependency to the root Cargo.toml.",
197 package_name,
198 dep_name
199 );
200 }
201 };
202 let check_table_like = |t: &dyn TableLike, dep_name| {
203 if t.get("workspace").and_then(|x| x.as_bool()) != Some(true) {
204 handle_non_workspaced_dep(dep_name);
205 }
206 };
207
208 for table in dep_tables {
209 for (dep_name, value) in table.iter() {
210 match value {
211 Item::Value(Value::String(_)) => handle_non_workspaced_dep(dep_name),
212 Item::Value(Value::InlineTable(t)) => {
213 check_table_like(t, dep_name);
214
215 if t.len() == 1 {
216 found_bad_deps.set(true);
217 log::error!(
218 "{} uses inline table syntax for its dependency on {}, but only contains one table entry. Please change to the dotted syntax.",
219 package_name,
220 dep_name
221 );
222 }
223 }
224 Item::Table(t) => check_table_like(t, dep_name),
225
226 _ => unreachable!(),
227 }
228 }
229 }
230
231 if found_bad_deps.get() {
232 Err(anyhow::anyhow!("Found incorrectly defined dependencies."))
233 } else {
234 Ok(())
235 }
236}